﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Reflection;
using System.Linq.Expressions;
using Test.Expressions;

namespace Test
{
	public class DbQueryProvider : QueryProvider, IQueryText
	{
		DbConnection connection;
		IQueryProvider provider;//THE .NET LINQ provider for LINQ to Objects

		public DbQueryProvider(DbConnection connection) {
			this.connection = connection;
			this.provider = new object[0].AsQueryable().Provider;
		}

		
		public override object Execute(Expression node) {
			Type elementType, returnType;
			TSQLTranslator translator;
			Dictionary<Type, ConstantExpression> map = new Dictionary<Type, ConstantExpression>();
			Dictionary<Type, List<ColumnExpression>> type_column_map = new Dictionary<Type, List<ColumnExpression>>();
			Dictionary<FieldInfo, string> field_alias_map;
			string TSQL, commandTSQL;

			ExpressionRewriter rewriter = new ExpressionRewriter(node);
			var firstPass = rewriter.Rewrite();
			field_alias_map = rewriter.field_alias_map;
			
			foreach (var c in firstPass.Columns) {
				if (!type_column_map.ContainsKey(c.DeclaringType)) {
					type_column_map.Add(c.DeclaringType, new List<ColumnExpression> { c });
				}
				else {
					type_column_map[c.DeclaringType].Add(c);
				}
			}
			translator = new TSQLTranslator(firstPass, field_alias_map);
			TSQL = translator.Translate();
			
			//todo: getting all objects can be done in one T-SQL statement / call to the database
			foreach (Type elementTypeKey in type_column_map.Keys) {
				commandTSQL = TSQL;//new DbQueryTSQLFormatter(columnMap[elementTypeKey], TSQL).Translate();
				System.Collections.IEnumerable objects = ExecuteReader(commandTSQL, elementTypeKey, field_alias_map);
				map.Add(elementTypeKey, Expression.Constant(objects.Cast(elementTypeKey)));
			}

			Expression secondPass = new ConstantExpressionRewriter(node, map.Values).Rewrite();

			switch (node.NodeType) {
				case ExpressionType.Call:
					returnType = ((MethodCallExpression)node).Method.ReturnType;
					if (returnType.IsScalar()) {
						return provider.Execute(secondPass);
					}
					break;
				case ExpressionType.Constant:
					returnType = ((IQueryable)((ConstantExpression)node).Value).ElementType;
					break;
				default:
					returnType = node.Type;
					break;
			}

			ParameterExpression p = Expression.Parameter(typeof(Expression), "p");
			elementType = TypeHelper.GetElementType(returnType);
			returnType = typeof(IEnumerable<>).MakeGenericType(elementType);
			MethodInfo mi = this.provider.GetType().GetInterface("IQueryProvider").GetMethods()[3]
				.MakeGenericMethod(returnType);
			MethodCallExpression call = Expression.Call(Expression.Convert(Expression.Constant(this.provider), typeof(IQueryProvider)),
				mi,
				Expression.Constant(secondPass));

			LambdaExpression lambda = Expression.Lambda(call, p);
			Delegate del = lambda.Compile();
			object result = del.DynamicInvoke(secondPass);
			return result;
		}

		System.Collections.IEnumerable ExecuteReader(string commandTSQL, Type elementType, Dictionary<FieldInfo,string> field_alias_map) {
			DbCommand cmd;
			DbDataReader reader;
			LambdaExpression lambda;
			System.Collections.IEnumerable objectReader;
			Type objectReaderType = typeof(ObjectReader<>).MakeGenericType(elementType);

			cmd = this.connection.CreateCommand();
			cmd.CommandText = commandTSQL;
			reader = cmd.ExecuteReader();

			//todo: instead compile this once as a NewExpression 
			ConstructorInfo ctor = typeof(ObjectReader<>).MakeGenericType(elementType).GetConstructors()[0];
			ParameterExpression p1 = Expression.Parameter(typeof(DbDataReader));
			ParameterExpression p2 = Expression.Parameter(typeof(Dictionary<FieldInfo, string>));
			NewExpression newexp = Expression.New(ctor, p1, p2);
			lambda = Expression.Lambda(newexp, p1, p2);
			objectReader = (System.Collections.IEnumerable)lambda.Compile().DynamicInvoke(reader, field_alias_map);

			MethodCallExpression toArray = Expression.Call(typeof(Enumerable),
				"ToArray",
				new Type[] { elementType },
				Expression.Constant(objectReader));
			lambda = Expression.Lambda(toArray, Expression.Parameter(typeof(IEnumerable<>).MakeGenericType(elementType), "p"));
			var array = (System.Collections.IEnumerable)lambda.Compile().DynamicInvoke(new object[] { objectReader });
			return array;
		}

		public override string GetQueryText(Expression expression) {
			SubqueryExpression rewrite = new ExpressionRewriter(expression).Rewrite();
			string TSQL = new TSQLTranslator(rewrite).Translate();
			return TSQL;
		}

	}
}
